package org.example.backend.utils;

import io.minio.*;
import io.minio.messages.Item;
import lombok.extern.slf4j.Slf4j;
import org.example.backend.bean.VideoChunk;
import org.example.backend.dto.VideoDTO;
import org.example.backend.service.VideoChunkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
 * @author 啊昌
 * @date 2025/7/21 16:59
 * FileUtils 文件工具类
 */
@Component
@Slf4j
public class FileUtils{

	@Autowired
	private MinioUtil minioUtil;

	private  final int DEFAULT_CHUNK_SIZE =5 * 1024 * 1024; // 默认5MB

	@Resource
	private MinioClient minioClient;


	@Autowired
	private FfmpegUtils ffmpegUtils;

	@Autowired
	private VideoChunkService videoChunkService;

	/**
	 * 计算给定文件的MD5值
	 *
	 * @param file 要计算MD5值的文件
	 * @return 文件的MD5值，以十六进制字符串形式表示
	 * @throws Exception 如果发生异常，则抛出异常
	 */
	public  String calculateMD5(MultipartFile file) throws Exception {
		MessageDigest md = MessageDigest.getInstance("MD5");
		try (InputStream is = file.getInputStream()) {
			byte[] buffer = new byte[8192];
			int bytesRead;
			while ((bytesRead = is.read(buffer)) != -1) {
				md.update(buffer, 0, bytesRead);
			}
		}
		return bytesToHex(md.digest());
	}

	// 优化字节转十六进制方法
	private  String bytesToHex(byte[] bytes) {
		StringBuilder hexString = new StringBuilder(2 * bytes.length);
		for (byte b : bytes) {
			String hex = Integer.toHexString(0xff & b);
			if (hex.length() == 1) {
				hexString.append('0');
			}
			hexString.append(hex);
		}
		return hexString.toString();
	}

	/**
	 * 将视频分片合并到数据库中
	 *
	 * @param bucketName   存储桶名称
	 * @param chunksPrefix 分片前缀
	 * @param totalChunks  分片总数
	 * @param fileMD5      文件的MD5值
	 * @return 包含所有分片的列表
	 * @throws Exception 如果发生异常，则抛出异常
	 */
	public List<VideoChunk> mergeChunksToDB(String bucketName, String chunksPrefix, int totalChunks,String fileMD5,String destObjectName) throws Exception {
		//将分片的记录存储在数据库中
		List<VideoChunk> videoChunks = new ArrayList<>();
		for (int i = 0; i < totalChunks; i++) {
			//totalChunks 就算总的分片大小
			VideoChunk videoChunk = new VideoChunk();
			String partName = chunksPrefix + (1+i) + ".tmp";
			//先查询得到存储在minio的分片
			String previewFileUrl = minioUtil.getPreviewFileUrl(bucketName, partName);
			String previewImg = previewFileUrl.substring(0, previewFileUrl.lastIndexOf("?"));
			videoChunk.setVideoId(null);//关联的视频id
			videoChunk.setChunkSize(null);//分片的大小
			videoChunk.setBucketName(bucketName); //所在minio的桶位置
			videoChunk.setChunkPath(partName); //分片在minio存储的对象名
			videoChunk.setDestObjectName(destObjectName); //合并的对象名字
			videoChunk.setChunkIndex(i);//分片下标
			videoChunk.setTotalChunks(totalChunks); //分片总的大小
			videoChunk.setChunkUrl(previewImg); //分片在minio存储在url地址
			videoChunk.setCreateTime(new Date());
			videoChunk.setUpdateTime(new Date());
			videoChunk.setUploadStatus(0);//上传状态：0-未上传，1-上传中，2-已上传，3-上传失败
			videoChunk.setMd5(fileMD5); //文件md5
			videoChunks.add(i, videoChunk);
		}
		return videoChunks;
	}
	/**
	 * 在MinIO中合并分片为完整文件
	 *
	 * @param bucketName     MinIO桶名称
	 * @param chunksPrefix   MinIO中分片文件的前缀 (例如: userId/videoId/chunks/)
	 * @param destObjectName 合并后的目标对象名 (例如: userId/videoId/video.mp4)
	 * @param totalChunks    总分片数
	 * @return 是否合并成功
	 */
	public boolean mergeChunksInMinio(String bucketName, String chunksPrefix, String destObjectName, int totalChunks) {
		try {
			log.info("开始在MinIO上合并分片，桶: {}, 分片前缀: {}, 目标文件: {}", bucketName, chunksPrefix, destObjectName);
			// 准备分片源对象列表
			List<ComposeSource> sourceObjectList = new ArrayList<>();
			for (int i = 1; i <= totalChunks; i++) {
				String partName = chunksPrefix + i + ".tmp";
				ComposeSource source = ComposeSource.builder()
						.bucket(bucketName)
						.object(partName)
						.build();
				sourceObjectList.add(source);
			}

			// 执行合并操作
			minioUtil.mergeChunksVideo(bucketName, destObjectName, sourceObjectList);
			log.info("分片合并成功，目标文件: {}/{}", bucketName, destObjectName);
			return true;
		} catch (Exception e) {
			log.error("MinIO分片合并失败: {}", e.getMessage(), e);
			return false;
		}
	}

	/**
	 * 合并视频分片到 MinIO
	 *
	 * @param videoChunks 分片列表
	 * @return 合并是否成功
	 */
	public boolean mergeChunksInMinio(List<VideoChunk> videoChunks) {
		if (videoChunks == null || videoChunks.isEmpty()) {
			log.warn("合并失败：videoChunks 为空");
			return false;
		}

		try {
			List<ComposeSource> sourceObjectList = new ArrayList<>();
			for (VideoChunk videoChunk : videoChunks) {
				sourceObjectList.add(
						ComposeSource.builder()
								.bucket(videoChunk.getBucketName())
								.object(videoChunk.getChunkPath()) // 这里要确认每个分片路径正确
								.build()
				);
			}

			VideoChunk videoChunk = videoChunks.get(0); // 用第一个分片获取桶名和目标路径
			String bucket = videoChunk.getBucketName();
			String destObjectName = videoChunk.getDestObjectName();

			log.info("✅ 开始在MinIO上合并分片，桶: {}, 分片前缀: {}, 目标文件: {}",
					bucket, videoChunk.getChunkPath(), destObjectName);

			ObjectWriteResponse response = minioUtil.mergeChunksVideo(bucket, destObjectName, sourceObjectList);

			if (response != null) {
				log.info("✅ 分片合并成功，目标文件: {}/{}", bucket, destObjectName);
				return true;
			} else {
				log.warn("⚠️ MinIO合并响应为空，可能未成功写入");
				return false;
			}

		} catch (Exception e) {
			log.error("❌ MinIO分片合并失败: {}", e.getMessage(), e);
			return false;
		}
	}

	/**
	 * 删除MinIO中的分片文件
	 *
	 * @param bucketName   MinIO桶名称
	 * @param chunksPrefix 分片文件前缀
	 * @return 删除的分片数量
	 */
	public int deleteChunks(String bucketName, String chunksPrefix) {
		try {
			int deletedCount = 0;
			// 列出所有分片文件
			Iterable<Result<Item>> results = minioClient.listObjects(
					ListObjectsArgs.builder()
							.bucket(bucketName)
							.prefix(chunksPrefix)
							.recursive(true)
							.build()
			);

			// 逐个删除分片
			for (Result<Item> result : results) {
				Item item = result.get();
				String objectName = item.objectName();
				minioUtil.deleteObject(bucketName, objectName);
				deletedCount++;
				log.info("删除分片文件: {}/{}", bucketName, objectName);
			}

			log.info("共删除 {} 个分片文件", deletedCount);
			return deletedCount;
		} catch (Exception e) {
			log.error("删除分片文件失败: {}", e.getMessage(), e);
			return 0;
		}
	}

	/**
	 * 将文件分片并上传到MinIO存储服务，然后合并分片，并生成封面图
	 *
	 * @param file   要上传的文件
	 * @param userId 用户ID，用作存储桶的名称
	 * @throws Exception 如果上传或合并过程中出现错误，将抛出异常
	 */
	public void splitFileToDB(MultipartFile file, String userId,String fileMD5) throws Exception {
		//创建分片文件夹 userId/文件md5/chunk/
		String bucketName = userId;
		minioUtil.createBucket(userId); //创建桶
		long fileSize = file.getSize(); //获取视频总的大小
		int totalChunks = (int) Math.ceil((double) fileSize / DEFAULT_CHUNK_SIZE);//计算分片的数量
		//开始上传文件
		InputStream inputStream = file.getInputStream();
		byte[] buffer = new byte[DEFAULT_CHUNK_SIZE];
		int bytesRead = 0; //读取的字节数
		int chunkIndex = 1;
		String chunkName = null;
		String objectName = fileMD5 + "/chunk/" + "part_";
		while ((bytesRead = inputStream.read(buffer, 0, DEFAULT_CHUNK_SIZE)) > 0) {
			//构造分片对象名
			chunkName = objectName + chunkIndex + ".tmp";
			// 如果不是完整的buffer，创建一个适当大小的byte数组
			byte[] dataToUpload = bytesRead == DEFAULT_CHUNK_SIZE ? buffer : new byte[bytesRead];
			if (bytesRead < DEFAULT_CHUNK_SIZE) {
				System.arraycopy(buffer, 0, dataToUpload, 0, bytesRead);
			}
			// 上传分片到MinIO
			try (ByteArrayInputStream bais = new ByteArrayInputStream(dataToUpload, 0, bytesRead)) {

				minioUtil.uploadFile(bais, bucketName, chunkName);
			}
			chunkIndex++;
		}
		// 检查是否所有分片都已上传
		if (chunkIndex - 1 != totalChunks) {
			log.warn("预期分片数与实际上传分片数不匹配! 预期: {}, 实际: {}", totalChunks, chunkIndex - 1);
		}
		// 2. 分片的数据得保留在数据库中
		List<VideoChunk> videoChunks = mergeChunksToDB(bucketName, objectName, totalChunks, fileMD5, fileMD5 + "/chunk/" + file.getOriginalFilename()); //将分片的数据保留在list集合中
		videoChunkService.saveVideoChunks(videoChunks); //todo 这里还少videoid 和 chunks_size地址没有填充

		//视频合并
		//todo 3. 这里得保留合并视频和封照片在数据库
//		boolean b = mergeChunksInMinio(bucketName, objectName, fileMD5 + "/chunk/" + file.getOriginalFilename(), totalChunks);
//		if (b) {
//			log.info("文件合并成功");
//			//保留视频的封面照片
//			// 上传封面图到 MinIO
//			String thumbnailName = file.getOriginalFilename();
//			String videoMinioPath = fileMD5 + "/chunk/" + file.getOriginalFilename();
//			//截图视频名字 比如20250621_154429.mp4 保留20250621_154429
////			 String thumbnailMinioPath= fileMD5 + "/thumbnail/" + file.getName().substring(0, file.getName().lastIndexOf(".")-1);
//			String fileName = file.getOriginalFilename();
//			int dotIndex = fileName.lastIndexOf("."); // 获取点号位置
//			String baseName = (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex); // 无点号时用全名
//			String thumbnailMinioPath = fileMD5 + "/thumbnail/" + baseName; // 拼接路径
//			generateThumbnail(bucketName, videoMinioPath, thumbnailName, thumbnailMinioPath);
//			log.info("封面图已成功上传到 MinIO");
//			//合并完成删除分片
//			deleteChunks(bucketName, objectName); //todo 4. 这里得删除保留在数据库的数据
//		} else {
//			log.error("文件合并失败");
//		}
	}


	/**
	 * 将文件分片并上传到MinIO存储服务，然后合并分片，并生成封面图
	 *
	 * @param file   要上传的文件
	 * @param userId 用户ID，用作存储桶的名称
	 * @throws Exception 如果上传或合并过程中出现错误，将抛出异常
	 */
	 public void  splitFile(MultipartFile file, String userId) throws Exception {
	 	//创建分片文件夹 userId/文件md5/chunk/
		 String bucketName = userId;
		 String fileMD5 = calculateMD5(file);
		 minioUtil.createBucket(userId); //创建桶
		 long fileSize = file.getSize(); //获取视频总的大小
		 int totalChunks = (int) Math.ceil((double) fileSize / DEFAULT_CHUNK_SIZE);//计算分片的数量
		 //开始上传文件
		 InputStream inputStream = file.getInputStream();
		 byte[] buffer = new byte[DEFAULT_CHUNK_SIZE];
		 int bytesRead = 0; //读取的字节数
		 int chunkIndex = 1;
		 String chunkName = null;
		 String objectName = fileMD5 + "/chunk/" + "part_";
		 while((bytesRead = inputStream.read(buffer, 0, DEFAULT_CHUNK_SIZE)) >0){
			 //构造分片对象名
			 chunkName = objectName + chunkIndex + ".tmp";
			 // 如果不是完整的buffer，创建一个适当大小的byte数组
			 byte[] dataToUpload = bytesRead == DEFAULT_CHUNK_SIZE ? buffer : new byte[bytesRead];
			 if (bytesRead < DEFAULT_CHUNK_SIZE) {
				 System.arraycopy(buffer, 0, dataToUpload, 0, bytesRead);
			 }
			 // 上传分片到MinIO
			 try (ByteArrayInputStream bais = new ByteArrayInputStream(dataToUpload, 0, bytesRead)) {
				 //todo 2. 分片的数据得保留在数据库中
				 minioUtil.uploadFile(bais, bucketName, chunkName);
			 }
			 chunkIndex++;
		 }
		 // 检查是否所有分片都已上传
		 if (chunkIndex - 1 != totalChunks) {
			 log.warn("预期分片数与实际上传分片数不匹配! 预期: {}, 实际: {}", totalChunks, chunkIndex - 1);
		 }
		 //视频合并
		 //todo 3. 这里得保留合并视频和面照片
		 boolean b = mergeChunksInMinio(bucketName, objectName, fileMD5 + "/chunk/"+file.getOriginalFilename(),totalChunks);
//		 boolean b = mergeChunksInMinio(bucketName, objectName, fileMD5 + "/chunk/"+"20250611_211455.mp4",totalChunks);
		 if(b){
			 log.info("文件合并成功");
			 //保留视频的封面照片
			 // 上传封面图到 MinIO
			 String thumbnailName =file.getOriginalFilename();
			 String videoMinioPath= fileMD5 + "/chunk/" + file.getOriginalFilename();
			 //截图视频名字 比如20250621_154429.mp4 保留20250621_154429
//			 String thumbnailMinioPath= fileMD5 + "/thumbnail/" + file.getName().substring(0, file.getName().lastIndexOf(".")-1);
			 String fileName = file.getOriginalFilename();
			 int dotIndex = fileName.lastIndexOf("."); // 获取点号位置
			 String baseName = (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex); // 无点号时用全名
			 String thumbnailMinioPath = fileMD5 + "/thumbnail/" + baseName; // 拼接路径
			 generateThumbnail(bucketName, videoMinioPath, thumbnailName, thumbnailMinioPath);
			 log.info("封面图已成功上传到 MinIO");
			 //合并完成删除分片
			 deleteChunks(bucketName, objectName); //todo 4. 这里得删除保留在数据库的数据
		 }else{
			 log.error("文件合并失败");
		 }
	 }


	/**
	 * 从 MinIO 下载视频文件，生成视频封面图并上传回 MinIO。
	 *
	 * @param bucketName         MinIO 存储桶名称
	 * @param objectName         MinIO 中对象的名称
	 * @param thumbnailName      封面图的名称
	 * @param thumbnailMinioPath 封面图在 MinIO 中的存储路径
	 * @throws Exception 抛出异常
	 */
		public  void generateThumbnail(String bucketName, String objectName, String thumbnailName,String thumbnailMinioPath) throws Exception {
			// 从 MinIO 下载视频文件的输入流
			InputStream videoInputStream = minioUtil.downLoad(bucketName, objectName);
			// 设置目标目录路径
			String videoDir = "F:\\project\\sp-bilibili\\backend\\tmp\\video\\";  // Windows 下的路径
			String imgDir = "F:\\project\\sp-bilibili\\backend\\tmp\\img";      // Windows 下的路径
			// 确保视频目录存在
			File videoFolder = new File(videoDir);
			if (!videoFolder.exists()) {
				videoFolder.mkdirs();  // 创建目录
			}
			// 确保封面图目录存在
			File imgFolder = new File(imgDir);
			if (!imgFolder.exists()) {
				imgFolder.mkdirs();  // 创建目录
			}
			// 拼接完整的视频文件路径
			String videoFilePath = videoDir + bucketName + "_" + thumbnailName + ".mp4";
			String thumbnailFilePath = imgDir + bucketName + "_" + thumbnailName + ".jpg";
			// 创建输出流将视频写入本地
			try (FileOutputStream fileOutputStream = new FileOutputStream(videoFilePath)) {
				byte[] buffer = new byte[1024];
				int length;
				while ((length = videoInputStream.read(buffer)) > 0) {
					fileOutputStream.write(buffer, 0, length);
				}
			}
			// 使用 FFmpeg 生成封面图
			boolean success = ffmpegUtils.generateThumbnail(videoFilePath, thumbnailFilePath, 1);
			if (success) {
				// 上传封面图到 MinIO
				try (FileInputStream thumbnailInputStream = new FileInputStream(thumbnailFilePath)) {
					minioUtil.uploadFile(thumbnailInputStream, bucketName, thumbnailMinioPath + ".jpg");
				}
			}
			// 删除临时文件
			Files.delete(Paths.get(videoFilePath));
			Files.delete(Paths.get(thumbnailFilePath));
		}




	// 使用 FFmpeg 提取封面图
//	private String generateThumbnail(String bucketName, String objectName, String thumbnailName) throws Exception {
//		// 下载视频文件到本地临时文件
//		InputStream videoInputStream = minioUtil.downLoad(bucketName, objectName);
////		String t1 = "F:\\project\\sp-bilibili\\backend\\";
////		String t2 = "F:\\project\\sp-bilibili\\backend\\tmp\\img\\video.jpg";
//		String fileVideoPath = t1 + bucketName + thumbnailName;
//		String fileImgPath = t2 + bucketName + thumbnailName;
//		// 创建视频文件的目标文件夹（如果不存在）
//		File videoFolder = new File(t1);
//		if (!videoFolder.exists()) {
//			videoFolder.mkdirs();  // 创建文件夹
//		}
//
//		// 创建封面图的目标文件夹（如果不存在）
//		File imgFolder = new File(t2);
//		if (!imgFolder.exists()) {
//			imgFolder.mkdirs();  // 创建文件夹
//		}
//
//		// 创建输出流，将文件写入本地
//		FileOutputStream fileOutputStream = new FileOutputStream("local_filename.mp4");
//		// 定义缓冲区大小
//		byte[] buffer = new byte[1024];
//		int length;
//		// 读取输入流并写入到本地文件
//		while ((length = videoInputStream.read(buffer)) > 0) {
//			fileOutputStream.write(buffer, 0, length);
//		}
//		// 关闭流
//		fileOutputStream.close();
//		videoInputStream.close();
//
//		// 使用 FFmpeg 生成封面图
//		boolean b = ffmpegUtils.generateThumbnail(t1+"local_filename.mp4", t2, 1);
//		if(b){
//			//封面图上传在本地功能
//		}
//		// 删除临时文件
////		Files.delete(tempVideoFile);
////		Files.delete(tempThumbnailFile);
//
//		return "fileImgPath";
//	}



	/**
	 * 将指定文件按指定大小拆分成多个文件片段
	 *
	 * @param sourceFilePath 源文件路径
	 * @param targetDirPath  目标目录路径
	 * @return 拆分后的文件片段数量
	 * @throws IOException 如果发生I/O错误
	 */
	public int splitFile(String sourceFilePath, String targetDirPath) throws IOException {
		Path sourcePath = Paths.get(sourceFilePath);
		if (!Files.exists(sourcePath)) {
			 // 源文件不存在
		}
		Path targetDir = Paths.get(targetDirPath);
		if(!Files.exists(targetDir)){
			//不存在则创建
		 Files.createDirectories(targetDir);//创建文件夹
		}

		FileInputStream inputStream = new FileInputStream(sourcePath.toFile());
		byte[] buffer = new byte[DEFAULT_CHUNK_SIZE];
		int bytesRead;
		int partNumber = 1;

		while ((bytesRead = inputStream.read(buffer)) > 0) {
			Path chunkPath = targetDir.resolve("part_" + String.format("%03d", partNumber) + ".tmp");
			try (OutputStream outputStream = new FileOutputStream(chunkPath.toFile())) {
				outputStream.write(buffer, 0, bytesRead);
			}
			partNumber++;
		}
		return partNumber -1 ;
	}

	/**
	 * 将指定目录下的多个分片文件合并为一个完整的文件。
	 *
	 * @param chunkDirPath   分片文件所在的目录路径
	 * @param outputFilePath 合并后文件的输出路径
	 * @throws IOException 如果在文件操作中发生I/O错误
	 */
	public void mergeChunks(String chunkDirPath, String outputFilePath) throws IOException {
		Path chunkDir = Paths.get(chunkDirPath);
		if (!Files.exists(chunkDir)) {
			// 分片的源文件不存在
		}

		Path outputPath = Paths.get(outputFilePath);
		if (Files.exists(outputPath)) {
			Files.delete(outputPath); // 删除旧文件
		}

		try (OutputStream out = new FileOutputStream(outputPath.toFile())) {
			int partNumber = 1;
			while (true) {
				Path chunkPath = chunkDir.resolve("part_" + String.format("%03d", partNumber) + ".tmp");
				if (!Files.exists(chunkPath)) {
					break;
				}

				try (InputStream in = new FileInputStream(chunkPath.toFile())) {
					byte[] buffer = new byte[1024 * 1024];
					int bytesRead;
					while ((bytesRead = in.read(buffer)) > 0) {
						out.write(buffer, 0, bytesRead);
					}
				}

				partNumber++;
			}
		}

		System.out.println("文件合并完成，输出路径：" + outputFilePath);
	}

	/**
	 * 计算给定文件的 MD5 哈希值。
	 *
	 * @param file 要计算 MD5 哈希值的文件
	 * @return 文件的 MD5 哈希值，以十六进制字符串表示
	 * @throws Exception 如果在读取文件或计算 MD5 哈希值时发生错误，则抛出异常
	 */
	public  String calculateMD5(File file) throws Exception {
		MessageDigest md = MessageDigest.getInstance("MD5");
		try (InputStream is = new FileInputStream(file);
		     BufferedInputStream bis = new BufferedInputStream(is)) {
			byte[] buffer = new byte[8192];
			int bytesRead;
			while ((bytesRead = bis.read(buffer)) != -1) {
				md.update(buffer, 0, bytesRead);
			}
		}
		byte[] digest = md.digest();
		StringBuilder sb = new StringBuilder();
		for (byte b : digest) {
			sb.append(String.format("%02x", b));
		}
		return sb.toString();
	}
}
